#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#include "lwip/err.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include "lwip/netdb.h"
#include "lwip/dns.h"

#include "sdkconfig.h"
#include "esp_sntp.h"
#include "esp_tls.h"
#include "esp_crt_bundle.h"
#include "http_request.h"

#define LOG_TAG "http"
static const char *TAG = "HTTP";
#include "esp_log.h"
// #include "config.h"

#ifdef CONFIG_SPIRAM
#define _malloc_(size) heap_caps_malloc(size, MALLOC_CAP_SPIRAM)
#else
#define _malloc_(size) malloc(size)
#endif

// #define HTTP_CLIENT_ENABLE

#define REQUEST "%s %s HTTP/1.1\r\n"                \
                "Host: %s\r\n"                      \
                "User-Agent: TTG/1.0 (Tantron)\r\n" \
                "Accept: */*\r\n"                   \
                "Connection: keep-alive\r\n"
#ifdef HTTP_CLIENT_ENABLE

int8_t http_request(const char *method, char *url, char *server, char *port, char *path, const char *data, uint32_t data_len, http_stream_cb_t cb, void *user_cb, http_header_cb_t header_cb)
{
#define RECV_DATA_SIZE 4096
    if (!server || !port || !path)
        return 0;
    char *buffer = _malloc_(RECV_DATA_SIZE);
    if (!buffer)
        return 0;

    bool is_ssl = (strcmp(port, "443") == 0);
    esp_tls_t *tls = NULL;
    int err = 0;
    int s = -1, r = 0;
    uint32_t header_len = sprintf(buffer, REQUEST, method, path, server);
    if (data)
    {
        header_len += sprintf(&buffer[header_len], "Content-Type: application/json\r\n");
        header_len += sprintf(&buffer[header_len], "Content-Length: %ld\r\n", data_len);
        header_len += sprintf(&buffer[header_len], "\r\n");
        header_len += sprintf(&buffer[header_len], "%s", data);
        header_len += sprintf(&buffer[header_len], "\r\n\r\n");
    }
    else
        header_len += sprintf(&buffer[header_len], "\r\n");
    // printf(buffer);

    if (!is_ssl)
    {
        const struct addrinfo hints = {
            .ai_family = AF_INET,
            .ai_socktype = SOCK_STREAM,
        };
        struct addrinfo *res;
        struct in_addr *addr;

        err = getaddrinfo(server, port, &hints, &res);

        if (err != 0 || res == NULL)
        {
            ESP_LOGE(TAG, "%s %s", server, port);
            ESP_LOGE(TAG, "DNS lookup failed err=%d res=%p", err, res);
            free(buffer);
            return 0;
        }

        /* Code to print the resolved IP.
           Note: inet_ntoa is non-reentrant, look at ipaddr_ntoa_r for "real" code */
        addr = &((struct sockaddr_in *)res->ai_addr)->sin_addr;
        ESP_LOGI(TAG, "DNS lookup succeeded. IP=%s", inet_ntoa(*addr));

        s = socket(res->ai_family, res->ai_socktype, 0);
        if (s < 0)
        {
            ESP_LOGE(TAG, "... Failed to allocate socket.");
            freeaddrinfo(res);
            goto http_get_err;
        }
        ESP_LOGI(TAG, "... allocated socket");

        if (connect(s, res->ai_addr, res->ai_addrlen) != 0)
        {
            ESP_LOGE(TAG, "... socket connect failed errno=%d", errno);
            goto http_get_err;
        }

        ESP_LOGI(TAG, "... connected");
        freeaddrinfo(res);

        ESP_LOGI(TAG, "... sending HTTP request...");
        if (write(s, buffer, header_len) < 0)
        {
            ESP_LOGE(TAG, "... socket send failed");
            goto http_get_err;
        }
        ESP_LOGI(TAG, "... socket send success");
    }
    else
    {
        esp_tls_cfg_t cfg = {
            .crt_bundle_attach = esp_crt_bundle_attach,
            .timeout_ms = 10000,
        };
        tls = esp_tls_init();
        if (!tls)
        {
            ESP_LOGE(TAG, "Failed to allocate esp_tls handle!");
            goto http_get_err;
        }

        char cur_url[256] = {0};
        if (!url)
        {
            if (snprintf(cur_url, sizeof(cur_url), "https://%s%s", server, path) >= sizeof(cur_url))
            {
                ESP_LOGE(TAG, "URL too long");
                goto http_get_err;
            }
            url = cur_url;
        }
        printf("url=%s\n", url);
        if (esp_tls_conn_http_new_sync(url, &cfg, tls) == 1)
            ESP_LOGI(TAG, "Connection established...");
        else
        {
            ESP_LOGE(TAG, "Connection failed...");
            int esp_tls_code = 0, esp_tls_flags = 0;
            esp_tls_error_handle_t tls_e = NULL;
            esp_tls_get_error_handle(tls, &tls_e);
            /* Try to get TLS stack level error and certificate failure flags, if any */
            err = esp_tls_get_and_clear_last_error(tls_e, &esp_tls_code, &esp_tls_flags);
            if (err == ESP_OK)
                ESP_LOGE(TAG, "TLS error = -0x%x, TLS flags = -0x%x", esp_tls_code, esp_tls_flags);
            goto http_get_err;
        }

        size_t written_bytes = 0;
        do
        {
            err = esp_tls_conn_write(tls,
                                     buffer + written_bytes,
                                     header_len - written_bytes);
            if (err >= 0)
            {
                ESP_LOGI(TAG, "%d bytes written", err);
                written_bytes += err;
            }
            else if (err != ESP_TLS_ERR_SSL_WANT_READ && err != ESP_TLS_ERR_SSL_WANT_WRITE)
            {
                ESP_LOGE(TAG, "esp_tls_conn_write  returned: [0x%02X](%s)", err, esp_err_to_name(err));
                goto http_get_err;
            }
        } while (written_bytes < header_len);

        if (esp_tls_get_conn_sockfd(tls, &s) != ESP_OK)
        {
            ESP_LOGE(TAG, "Failed to get socket fd");
            goto http_get_err;
        }
    }

    /* Read HTTP response */
    uint8_t timeout_count = 0;
    uint8_t check_header = 0;
    uint32_t data_size = 0xFFFFFFFF;
    do
    {
        bzero(buffer, RECV_DATA_SIZE);
        if (!is_ssl)
        {
            struct timeval tv = {
                .tv_sec = 1,
                .tv_usec = 0,
            };
            fd_set rfds;
            FD_ZERO(&rfds);
            FD_SET(s, &rfds);

            int sock = select(s + 1, &rfds, NULL, NULL, &tv);
            if (sock < 0)
            {
                ESP_LOGE(TAG, "Select failed: errno %d", errno);
                err = -1;
                break;
            }
            if (sock == 0)
            {
                ESP_LOGE(TAG, "Receive a timeout");
                if (++timeout_count > 5)
                    break;
                continue;
            }
            if (!FD_ISSET(s, &rfds))
                continue;

            struct sockaddr_storage raddr; // Large enough for both IPv4 or IPv6
            socklen_t socklen = sizeof(raddr);
            r = recvfrom(s, buffer, RECV_DATA_SIZE, 0, (struct sockaddr *)&raddr, &socklen);
            if (!r)
            {
                err = 1;
                break;
            }
        }
        else
        {
            err = esp_tls_conn_read(tls, (char *)buffer, RECV_DATA_SIZE);
            if (err == ESP_TLS_ERR_SSL_WANT_WRITE || err == ESP_TLS_ERR_SSL_WANT_READ)
                continue;
            else if (err < 0)
            {
                ESP_LOGE(TAG, "esp_tls_conn_read  returned [-0x%02X](%s)", -err, esp_err_to_name(err));
                break;
            }
            else if (err == 0)
            {
                ESP_LOGI(TAG, "connection closed");
                break;
            }
            r = err;
        }

        char *data = buffer;
        if (!check_header)
        {
            char *content = buffer;
            data = strstr(buffer, "\r\n\r\n");
            if (data)
            {
                check_header = 1;
                data[0] = '\0';
                data += 4;
                r -= (data - buffer);
            }
            ESP_LOGI(TAG, "... received HTTP response...");
            while ((content = strstr(content, "Content-")))
            {
                check_header = 1;
                char *key = content;
                char *value = strchr(key, ':');
                content = strstr(content, "\n"); /* end */
                value[0] = '\0';
                content[0] = '\0';
                value++;
                content++;
                if (header_cb)
                    header_cb(user_cb, key, value);
                if (strnstr(key, "Length", 14)) /* Content-Length: */
                    data_size = atol(value);
                printf("%s:%s\n", key, value);
            }
        }
        data_size -= r;
        if (cb)
        {
            if (is_ssl)
            {
                char *newLine = strnstr(data, "\r\n", 16);
                if (newLine)
                {
                    uint32_t chunk_size = atol(data);
                    r -= (newLine + 2 - data);
                    if (chunk_size != r)
                    {
                        ESP_LOGW(TAG, "Chunk size %lu != read size %d", chunk_size, r);
                        r += (newLine + 2 - data);
                        ESP_LOG_BUFFER_HEX(TAG, data, r);
                    }
                    else
                        data = newLine + 2;
                }
            }
            // ESP_LOG_BUFFER_HEX(TAG, data, r);
            err = cb(user_cb, (uint8_t *)data, r);
            if ((uint8_t)err)
            {
                ESP_LOGW(TAG, "User cb close.");
                goto http_get_err;
            }
        }
        else
        {
            ESP_LOGW(TAG, "No find User cb.");
            buffer[r] = '\0';
            for (int i = 0; i < r; i++)
                putchar(buffer[i]);
        }
        if (data_size == 0)
            goto http_request_done;
    } while (r > 0);

http_request_done:
    if (cb)
        cb(user_cb, 0, 0); /* read done */

    ESP_LOGI(TAG, "... done reading from socket. Last read return=%d errno=%d.", r, errno);
    free(buffer);
    if (tls)
        esp_tls_conn_destroy(tls);
    if (!is_ssl && s != -1)
        close(s);

    return err;
http_get_err:
    free(buffer);
    if (tls)
        esp_tls_conn_destroy(tls);
    if (!is_ssl && s != -1)
        close(s);
    return err - 1;
}

int8_t http_get(char *server, char *url, char *port, char *path, const char *data, uint32_t data_len, http_stream_cb_t cb, void *user_cb, http_header_cb_t header_cb)
{
    return http_request("GET", url, server, port, path, data, data_len, cb, user_cb, header_cb);
}

int8_t http_post(char *server, char *url, char *port, char *path, const char *data, uint32_t data_len, http_stream_cb_t cb, void *user_cb, http_header_cb_t header_cb)
{
    return http_request("POST", url, server, port, path, data, data_len, cb, user_cb, header_cb);
}

void http_get_url_info(char *url, char *server, char *port, char *path)
{
    strcpy(server, "127.0.0.1");
    strcpy(port, "80");
    strcpy(path, "/");
    char *source_url = url;
    url = strstr(source_url, "http://");
    if (!url)
    {
        url = strstr(source_url, "https://");
        if (url)
        {
            strcpy(port, "443");
            url += 8;
        }
        else
            url = source_url;
    }
    else
        url += 7;

    char *port_idx = strchr(url, ':');
    char *path_idx = strchr(url, '/');
    if (port_idx)
    {
        strncpy(server, url, port_idx - url);
        strncpy(port, port_idx + 1, path_idx - port_idx - 1);
    }
    else
        strncpy(server, url, path_idx - url);
    strcpy(path, path_idx);

    // printf("url:%s\n", source_url);
    // printf("find server : %s\n", server);
    // printf("find port : %s:%s\n", port_idx, port);
    // printf("find path : %s:%s\n", path_idx, path);

    // printf("url:%s\n", source_url);
    // printf("find server : %s\n", server);
    // printf("find port : %s\n", port);
    // printf("find path : %s\n", path);
}

int8_t http_request_from_url(const char *method, char *url, const char *data, uint32_t data_len, http_stream_cb_t cb, void *user_cb, http_header_cb_t header_cb)
{
    if (!url || !cb)
        return -1;
    char server[256] = "127.0.0.1";
    char port[6] = "80";
    char path[256] = "/";
    http_get_url_info(url, server, port, path);
    return http_request(method, url, server, port, path, data, data_len, cb, user_cb, header_cb);
}

// #define HTTP_REQ_TEST
#ifdef HTTP_REQ_TEST

int main(int argc, char *argv[])
{
    // char *url = "http://192.168.15.200:7892/app.bin";
    char *url = "https://gitee.com/maplerian/esp-bat-pb-idf/raw/master/bin/${version}/app.bin";
    char server[256] = "127.0.0.1";
    char port[6] = "80";
    char path[256] = "/";
    http_get_url_info(url, server, port, path);
    return 0;
}
#endif

#endif
